// Copyright 2010-2012, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef LIBV_SFILE_H_
#define LIBV_SFILE_H_

#include <stdio.h>
#include <assert.h>
#include <errno.h>           // EOPNOTSUPP, ENOSYS

#include <algorithm>         // min
#include <limits>            // numeric_limits::max(), numeric_limits::min()
#include <string>
#include <vector>

#include <libv/check.h>
#include <libv/cast.h>
#include <libv/suppress.h>
#include <libv/uncopyable.h>

#if defined(_WIN32)
#include <libv/windows_lean.h>
# include <io.h>             // _get_osfhandle, _open, _close
# include <fcntl.h>          // _O_CREAT, _O_EXCL, _O_RDWR
# include <sys/stat.h>       // _S_IREAD | _S_IWRITE

// Требуется одинаковая семантика на Windows и Linux, так что используем
// системные вызовы только из стандарта (fopen, open, а не их _s аналоги).
# if defined(_MSC_VER) && _MSC_VER >= 1200
#  pragma warning(push)
#  pragma warning(disable:4996)
# endif

typedef fpos_t           fpos64;
typedef unsigned __int64 fsize;
typedef HANDLE           FileHandle;


#elif defined(__linux__)
# include <stdint.h>
# include <sys/types.h>       // open
# include <sys/stat.h>        // open, S_IRUSR
# include <fcntl.h>           // open, posix_fallocate
# include <sys/syscall.h>     // syscall, fallocate
# include <unistd.h>          // syscall, fallocate, ftruncate

// Если этот файл не будет найден — Linux очень старый, закомментировать строку.
// Все будет работать, но с явным занулением (линейное время преаллокации).
# include <linux/falloc.h>    // syscall, fallocate

# define _FILE_OFFSET_BITS 64
# ifndef _XOPEN_SOURCE
#  define _XOPEN_SOURCE 600   // fallocate
# endif

typedef off64_t    fpos64;
typedef uint64_t   fsize;
typedef int        FileHandle;


#else
# error "This header should be used in Windows or Linux 2.6+."
#endif


namespace libv
{

/** Переносимый (Windows, Linux) класс для хранения бинарных данных.
  * За счет буферизации stdio эффективно работает как с небольшими,
  * так и с крупными объемами данных.
  * Совместим с FILE* (не добавляет служебную информацию в файл).
  * Реализован с использованием const-перегрузок:
  *   - константный экземпляр класса работает с файлом только для чтения
  *   - не константный — для чтения и записи.
  *
  * Благодаря проверкам компилятора на const-корректность программы, повышается
  * надежность, ценой возможности открывать одним объектом файлы в разных
  * режимах (чтения и записи).
  *
  * Класс не потокобезопасен. Исключения не генерирует. Смещения 64-битные.
  *
  * Общий план методов (транзакционный):
  *   - закрытие файла (для Create и Open)
  *   - проверка аргументов на выполнение предусловий
  *   - выполнение системных вызовов
  *   - переход в новое состояние в случае успеха.
  *
  * Коды возврата в функцях:
  *   -  0 успешное выполнение
  *   - -1 ошибка в системном вызове, код можно получить через last_error()
  *   - -2 аргументы не соблюдают предусловия.
  *
  * Опционально доступны:
  *  - автоудаление файлов после закрытия (или в деструкторе)
  *  - преаллокация (резервирования места на диске).
  *
  * По умолчанию:
  *  - файл не удаляется после закрытия
  *  - преаллокация установлена в режим kRecPrealloc, выделяющий только
  * необходимое количество байтов непосредственно перед записью.
  *
  * После Close() все поля кроме last_error_ принимают в значения по умолчанию.
  *
  * Класс определяет 3 псевдонима:
  *  - fpos64 (int64_t) для 64-битного смещения в файле
  *  - fsize (uint64_t) для размера файла
  *  - FileHandle (HANDLE или int) для системного хендла файла.
  *
  * @note Текстовый режим намеренно не поддерживается, т.к. понятие
  * «смещение» в нем не определено (проблема в символе новой строки).
  * Для лог-файлов рекомендуется использовать fopen(..., "a+"),
  * fwrite() + fflush() + fdatasync().
  *
  * @note Явное объявление всех методов inline необходимо не для встраивания
  * (оптимизации), а для команды линкеру на игнорирование ODR, то есть для
  * размещения определений методов класса вне определения класса. */
class SFILE : libv::Uncopyable
{
  public:
    struct PreallocType
    {
        static const int kInvalidPrealloc = 0;
        static const int kNoPrealloc = 1;
        static const int kRecPrealloc = 2;
        static const int kConstIncPrealloc = 3;
        static const int kPercIncPrealloc = 4;

        static bool is_valid(const int prealloc_type)
        {
            return prealloc_type == kNoPrealloc ||
                   prealloc_type == kRecPrealloc ||
                   prealloc_type == kConstIncPrealloc ||
                   prealloc_type == kPercIncPrealloc;
        }
    };


    /** Инициализация полей класса. */
    inline SFILE();

    /** Деструктор, вызывающий для открытого файла Close() без проверки ошибок.
      * @see Close(). */
    inline ~SFILE();

    /** Закрытие текущего файла (если есть) и атомарное создание нового.
      * Если файл уже существует возвращается -1.
      * Вторым аргументом можно задать права создаваемого файла в восьмиричном
      * Unix-формате (3 цифры с впередистоящим нулем).
      * Макросы использовать не рекомендуется, т.к. они платформозависимы.
      * В Windows файл будет создан исходя из прав пользователя (первая цифра).
      * @pre !filepath.empty()
      * @pre mode >= 0 && mode <= 0777.
      * @post is_opened() == true.
      * @param[in] filepath путь к создаваемому файлу
      * @param[in] mode права создаваемого файла в Unix-стиле.
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -2 аргументы не соблюдают предусловие.
      * @note В Windows нельзя создавать файл только для записи, такие права
      * будут трактоваться как чтение-запись.
      * @note В случае ошибки, класс переводится в обнуленное состояние.
      * @note по умолчанию используется kRecPrealloc преаллокация.
      * @warning при использовании NFSv1 и NFSv2 атомарность создания не
      * гарантируется, версии, начиная с NFSv3 (1995 г.) работают корректно. */
    inline int Create(const ::std::string& filepath, const int mode = 0644);

    /** Закрытие текущего файла и открытие существующего для записи.
      * @pre !filepath.empty().
      * @post is_opened() == true.
      * @param[in] filepath путь к открываемому файлу.
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -2 аргументы не соблюдают предусловие.
      * @note по умолчанию используется kRecPrealloc преаллокация.
      * @note В случае ошибки, класс переводится в обнуленное состояние. */
    inline int Open(const ::std::string& filepath);

    /** Закрытие текущего файла и открытие существующего для чтения.
      * @pre !filepath.empty().
      * @post is_opened() == true.
      * @param[in] filepath путь к открываемому файлу.
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -2 аргументы не соблюдают предусловие.
      * @note по умолчанию используется kRecPrealloc преаллокация.
      * @note В случае ошибки, класс переводится в обнуленное состояние. */
    inline int Open(const ::std::string& filepath) const;

    /** Проверка того, что объект ссылается на открытый файл.
      * @note false обозначает, что объект находися в обнуленном состоянии. */
    inline bool is_opened() const;

    /** Изменение размера файла.
      * Если max_off меньше текущего размера, конец файла будет отброшен.
      * Если max_off больше текущего размера, файл будет увеличен, причем
      * по возможности, ОС не будет занулять «хвост», а сдвинет указатель
      * конца файла на уровне системной ФС.
      * В случае возникновения ошибки размер файла не определен.
      * @pre is_opened() == true
      * @pre max_off >= 0.
      * @param[in] max_off новое максимальное смещение (размер) файла.
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -2 аргументы не соблюдают предусловие.
      * @note Функцию следует использовать для явной преаллокации. */
    inline int Resize(const fpos64 max_off);

    /** Функция, игнорируя ошибки, последовательно вызывает:
      *  - Flush()
      *  - fclose(3)
      *  - remove(3) (если включено автоудаление файлов)
      *  - Reset().
      * В случае возникновения ошибок в системных вызовах, last_error() вернет
      * код последней из них.
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код.
      *
      * После завершения метода класс переходит в обнуленное состояние.
      * @note Рекомендуется первые два вызова производить вручную, тогда можно
      * будет установить источник ошибки. */
    inline int Close();

    /** Функция, игнорируя ошибки, последовательно вызывает:
      *  - fclose(3)
      *  - Reset().
      * @see Close() */
    inline int Close() const;

    /** Запись данных в файл с заданного смещения.
      * Если offset отрицательный — считается, что он от конца файла.
      * Возвращаемый оффсет инкрементирован на data_size.
      * Так как при записи в конец, может понадобиться преаллокация, а, значит,
      * будет сдвинут конец файла, то возвращенный отрицательный оффсет может
      * потерять смысл. Поэтому, возвращаемый offset — всегда положительное
      * смещение (от начала файла) байта, следующего за последним записанным.
      * @pre is_opened() == true
      * @pre offset != NULL && data != NULL && data_size > 0
      * @pre *offset + Size() >= 0, при *offset < 0.
      * @post *offset инкрементирован на data_size и *offset >= 0.
      * @param[in,out] offset смещение, начиная с которого записываются данные
      * @param[in] data буфер с записываемыми данными
      * @param[in] data_size размер данных в буфере.
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -2 аргументы не соблюдают предусловие.
      * @note При включенной преаллокации и успешном завершении метода,
      * дальнейшая ошибка «нехватка места» (основная проблема в методах записи
      * с буферизацией) исключена. */
    inline int Write(fpos64* offset, const void* data, const size_t data_size);

    /** Чтение данных в файл с заданного смещения.
      * Если offset отрицательный — считается, что он от конца файла.
      * Возвращаемый оффсет инкрементирован на data_size.
      * @pre is_opened() == true
      * @pre offset != NULL && buffer != NUL && buffer_size > 0
      * @pre *offset + buffer_size <= Size(), при *offset >= 0 и
      *      *offset + buffer_size<=0 и *offset + Size() >= 0, при *offset < 0.
      * @post *offset инкрементирован на data_size.
      * @param[in,out] offset смещение, начиная с которого считываются данные
      * @param[in] data буфер, в который считываются данные
      * @param[in] data_size размер буфера и размер считываемых данных.
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код
      * @retval -2 аргументы не соблюдают предусловие.
      * @note Чтение незаписанных данных в общем случае вернет нули. */
    inline int Read(fpos64* offset,
                    void* buffer, const size_t buffer_size) const;

    /** Размер файла с учетом преаллокации.
      * @pre is_opened() == true
      * @retval >=0 размер файла
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код */
    inline fpos64 Size() const;

    /** Явный сброс всех данных на диск.
      * @pre is_opened() == true
      * @retval 0 успешное выполнение
      * @retval -1 произошла внутренняя ошибка, last_error() вернет ее код.
      * @note При использовании преаллокации вероятность ошибки в данном вызове
      * резко снижается. Если ошибка произошла — незаписанные данные утеряны.
      * @note В Linux мета-информация не обновляется. */
    inline int Flush();

    /** Управление опцией: удаление файла после закрытия.
      * @warning Опция сбрасывается в false после Close(). */
    inline void SetAutoRemove(const bool on);

    /** Изменение типа преаллокации.
      * Доступны (статические поля структуры PreallocType):
      * - kNoPrealloc преаллокация отключена (p_param игнорируется)
      * - kRecPrealloc перед занесением данных в буфер резервируется
      *   необходимое для текущей записи количество байтов
      *   (p_param игнорируется)
      * - kConstIncPrealloc при необходимости преаллокации, к максимальному
      *   смещению данных прибавляется p_param байтов
      * - kPercIncPrealloc при необходимости преаллокации, к максимальному
      *   смещению данных прибавляется p_param процентов байтов.
      * @pre PreallocType::is_valid(prealloc_type)
      * @pre p_param > 0, при kConstIncPrealloc или kPercIncPrealloc.
      * @param[in] prealloc_type тип преаллокации
      * @param[in] p_param параметр преаллокации:
      *   для kNoPrealloc и kRecPrealloc игнорируется,
      *   для kConstIncPrealloc объем добавляемых данных при необходимости,
      *   для kPercIncPrealloc процент от максимального требуемого объема,
      *   добавляемый в качестве резерва.
      * @retval 0 успешное выполнение
      * @retval -2 аргументы не соблюдают предусловие.
      * @note В случае достаточного количества места на диске, результаты
      * использования kNoPrealloc и kRecPrealloc не отличаются.
      * В случае нехватки места, при  использовании kRecPrealloc ошибка будет
      * обнаружена в момент вызова метода Write, когда пользователь еще не
      * передал свои данные, в то время как при использовании kNoPrealloc —
      * только в не определенный момент записи данных на диск (Flush).
      * @note Можно вызывать данный метод несколько раз для одного файла.
      * @warning Тип преаллокации сбрасывается в REC_PRALLOC после Close().
      * @see Resize(). */
    inline int SetPreallocType(const int prealloc_type, const fsize p_param);

    /** Получение кода последней ошибки в системных вызовах внутри методов.
      * Функцию следует вызывать, только после возврата методом кода -1. */
    inline int last_error() const;

    /** Получение хендла файла.
      * Использовать только для служебных целей (например, блокировок), но не в
      * обход методов класса. */
    inline FileHandle file_handle() const;

    /** Получение имени файла.
      * Если is_opened() == false возвращается пустая строка. */
    inline ::std::string file_path() const;

  private:
#ifdef _WIN32
    // @retval 1 достаточно прав для SetFileValidData.
    // @retval 0 недостаточно прав для SetFileValidData.
    // @retval -1 ошибка, код ошибки помещен в last_error.
    inline int CanSetFileValidDataBeUsed() const;
#endif


    /** Обнуление всех полей, кроме last_error_. */
    inline void Reset();

    /** Обнуление всех mutable полей, кроме last_error_. */
    inline void Reset() const;

    /** Проверка условия, что max_offset не выходит за границы преаллокации и
      * увеличение ее по правилам, определнным в prealloc_type_,
      * в противном случае. */
    inline int PreallocateCheck(const fpos64 max_offset);

    /** Переносимая версия fseek, принимающая 64-битное смещение. */
    inline int Fseek(const fpos64 where, const int origin) const;

    /** Переносимая версия ftell, принимающая 64-битное смещение. */
    inline fpos64 Ftell() const;



    /** Указатель на файл. */
    mutable FILE* file_;

    /** Путь к файлу (для удаления). */
    mutable ::std::string file_path_;

    /** Файл нужно удалить после закрытия. */
    bool auto_remove_;

    /** Тип преаллокации. */
    int prealloc_type_;

    /** Настройка для XXX_INC_PREALLOC. */
    fsize prealloc_settings_;

    /** Текущий закэшированный размер файла. */
    mutable fpos64 file_size_;

    /** Код последней ошибки. */
    mutable int last_error_;

    /** Системный хендл файла. */
    mutable FileHandle file_handle_;
};


SFILE::SFILE()
    : last_error_(0)
{
    Reset();  // всегда вызывается неконстантный Reset()
}


SFILE::~SFILE()
{
    Close();
}


bool SFILE::is_opened() const
{
    return file_ != NULL;
}


int SFILE::Close()
{
    // Проследим вызов fclose и все возможные ошибки:
    // -> fclose(3):
    // --> fflush(3): см. write(2)
    // ---> write(2): EAGAIN(-), EINVAL(-), EFAULT(-), EBADF(-), EPIPE(-),
    //                EFBIG(0), ENOSPC(+), EINTR(+), EIO(+),  ENXIO(+)
    // --> close(2): EBADF(-), EINTR(+), ENOLINK(-), EIO(+).
    // Обозначения:
    //   + — потенциальные ошибки,
    //   0 — маловероятные ошибки,
    //   - — невозможные ошибки.
    // Таким образом, с учетом того, что класс хранит корректный файловый
    // дескриптор на файл, открытый в режиме для записи, в современной
    // операционной системе, поддерживающей 64-битные смещения в файлах
    // (LFS, с 2000 года), все возможные ошибки связаны с:
    //   - ошибкой ввода/вывода на диск (EIO)
    //   - отсутствием устройства (ENXIO)
    //   - прерыванием, вызванным внешним сигналом (EINTR)
    //   - нехваткой свободного места на диске (ENOSPC).
    //   Первые 3 ошибки невозможно программно предотвратить, в тоже
    // время они довольно маловероятны при корректном использовании
    // программы на качественном оборудовании, что зависит исключительно
    // от пользователя.
    //   Что касается последней и наиболее потенциальной ошибки нехватки
    // места на диске, то вероятность ее можно значительно снизить, используя
    // преаллокацию.

    int r = 0;
    if ( is_opened() == false )
        r = -2;
    else
    {
        // пытаемся выполнить как можно больше методов перед занулением объекта
        if ( auto_remove_ == true )
        {
            if ( fclose(file_) == EOF )
            {
                last_error_ = errno;
                r = -1;
            }

            if ( remove(file_path_.c_str()) == -1 )
            {
                last_error_ = errno;
                r = -1;
            }
        }
        else
        {
            r = Flush();
            if ( fclose(file_) == EOF )
            {
                last_error_ = errno;
                r = -1;
            }
        }
    }

    Reset();
    return r;
}


int SFILE::Close() const
{
    int r = 0;
    if ( is_opened() == false )
        r = -2;
    else
        if ( fclose(file_) == EOF )
        {
            last_error_ = errno;
            r = -1;
        }

    Reset();
    return r;
}


fpos64 SFILE::Size() const
{
    if ( is_opened() == false )
        return -2;


    // Раньше метод всегда вызывал FSEEK(,,SEEK_END) + FTEEL(), но
    // исходя из профилирования это отнимало много ресурсов, поэтому размер
    // теперь кэшируется 1 раз и вычисляется в вызовах Resize и Write.
    if ( file_size_ == -1 )
    {
        // Через функции ОС (типа _filesize или fstat) делать нельзя, т.к. они
        // не учитывают буфер stdio (разве что fflush каждый раз вызывать).
        // Для текстового этот способ некорректен.
        if ( Fseek(0, SEEK_END) == -1 )
        {
            last_error_ = errno;
            return -1;
        }

        const fpos64 s = Ftell();
        if ( s == -1 )
        {
            last_error_ = errno;
            return -1;
        }

        file_size_ = s;
    }

    return file_size_;
}


int SFILE::Write(fpos64* offset, const void* data, const size_t data_size)
{
    if ( is_opened() == false ||
         offset == NULL || data == NULL || data_size == 0 )
        return -2;

    const fpos64 s = Size();
    if ( s == -1 )
        return -1;

    if ( *offset < 0 && *offset + s < 0 )
        return -2;

    const fpos64 b_offset = *offset < 0 ? s + *offset : *offset;
    int r = PreallocateCheck(b_offset + data_size);
    if ( r != 0 )
        return r;

    // перезаписываем данные, а не дополняем файл
    if ( Fseek(b_offset, SEEK_SET) == -1 ||
         fwrite(data, 1, data_size, file_) != data_size )
    {
        last_error_ = errno;
        return -1;
    }

    file_size_ = ::std::max<fpos64>(*offset + data_size, file_size_);
    *offset = b_offset + data_size;
    return 0;
}


int SFILE::Read(fpos64* offset, void* buffer, const size_t buffer_size) const
{
    if ( is_opened() == false ||
         offset == NULL || buffer == NULL || buffer_size == 0 )
        return -2;

    const fpos64 s = Size();
    if ( s == -1 )
        return -1;

    const fpos64 s_buf_size = libv::IntegerCast<fpos64>(buffer_size);
    if ( *offset >= 0 && *offset + s_buf_size > Size() )
        return -2;
    if ( *offset < 0 && ( *offset + s_buf_size > 0 || *offset + Size() < 0 ) )
        return -2;

    const fpos64 b_offset = *offset < 0 ? s + *offset : *offset;

    if ( Fseek(b_offset, SEEK_SET) == -1 ||
         fread(buffer, 1, buffer_size, file_) != buffer_size )
    {
        last_error_ = errno;
        return -1;
    }

    *offset = b_offset + buffer_size;
    return 0;
}


void SFILE::SetAutoRemove(const bool on)
{
    auto_remove_ = on;
}


int SFILE::SetPreallocType(const int prealloc_type,
                           const fsize prealloc_settings)
{
    if ( is_opened() == false )
        return -2;

    if ( PreallocType::is_valid(prealloc_type) == false )
        return -2;

    if ( ( prealloc_type == PreallocType::kConstIncPrealloc ||
           prealloc_type == PreallocType::kPercIncPrealloc ) &&
         prealloc_settings == 0 )
           return -2;

    prealloc_type_ = prealloc_type;
    prealloc_settings_ = prealloc_settings;
    return 0;
}


int SFILE::PreallocateCheck(const fpos64 max_offset)
{
    if ( max_offset < 0 )
        return -2;

    const fpos64 delta = max_offset - file_size_;
    if ( delta > 0 )
    {
        if ( prealloc_type_ == PreallocType::kNoPrealloc )
            return 0;
        else if ( prealloc_type_ == PreallocType::kRecPrealloc )
            return Resize(max_offset);
        else if ( prealloc_type_ == PreallocType::kConstIncPrealloc )
            return Resize(max_offset + prealloc_settings_);
        else if ( prealloc_type_ == PreallocType::kPercIncPrealloc )
            return Resize(max_offset * ( 100 + prealloc_settings_ ) / 100);

        libv::Die("Incorrect prealloc_type @ SFILE::PreallocCheck.");

        LIBV_SUPPRESS_INTEL_MISSING_RETURN(-1);
    }
    return 0;
}


void SFILE::Reset() const
{
    file_ = NULL;
    file_path_.clear();
    file_handle_ = 0;
    file_size_ = -1;
}


void SFILE::Reset()
{
    file_ = NULL;
    file_path_.clear();
    auto_remove_ = false;
    prealloc_type_ = PreallocType::kRecPrealloc;
    file_size_ = -1;
    prealloc_settings_ = 0;
    file_handle_ = 0;
}


int SFILE::last_error() const
{
    return last_error_;
}


FileHandle SFILE::file_handle() const
{
    return file_handle_;
}


::std::string SFILE::file_path() const
{
    return file_path_;
}


#if defined(_WIN32)


int SFILE::Create(const ::std::string& filepath, const int mode /* = 644 */)
{
    if ( is_opened() )
    {
        int r = Close();
        if ( r != 0 )
            return r;
    }

    if ( filepath.empty() || ! ( mode >= 0 && mode <= 0777 ) )
        return -2;

    int winmode = 0;
    if ( mode & 0100 )
        winmode |= _S_IEXEC;  // вероятно, в Windows для совместимости
    if ( mode & 0200 )
        winmode |= _S_IWRITE;
    if ( mode & 0400 )
        winmode |= _S_IREAD;

    // обязательно задаем _O_BINARY иначе 0x1A (^Z) воспринимается как EOF.
    const int fd = _open(filepath.c_str(),
                         _O_CREAT | _O_TRUNC | _O_EXCL | _O_RDWR | _O_BINARY,
                         winmode);
    if ( fd == -1 )
    {
        last_error_ = errno;
        return -1;
    }

    FILE* file = _fdopen(fd, "w+b");
    if ( file == NULL )
    {
        last_error_ = errno;
        _close(fd);
        return -1;
    }

    const HANDLE file_handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
    if ( file_handle == INVALID_HANDLE_VALUE )
    {
        last_error_ = errno;
        fclose(file);
        return -1;
    }

    file_ = file;
    file_path_ = filepath;
    file_size_ = 0;
    file_handle_ = file_handle;

    return 0;
}


int SFILE::Open(const ::std::string& filepath)
{
    if ( is_opened() )
    {
        int r = Close();
        if ( r != 0 )
            return r;
    }

    if ( filepath.empty() )
        return -2;

    FILE* file = fopen(filepath.c_str(), "r+b");
    if ( file == NULL )
    {
        last_error_ = errno;
        return -1;
    }

    const int fd = _fileno(file);
    if ( fd == -1 )
    {
        last_error_ = errno;
        fclose(file);
        return -1;
    }

    const HANDLE file_handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
    if ( file_handle == INVALID_HANDLE_VALUE )
    {
        last_error_ = errno;
        fclose(file);
        return -1;
    }

    file_ = file;
    const fpos64 s = Size();
    if ( s < 0 )
    {
        file_ = NULL;
        fclose(file);
        assert(s >= ::std::numeric_limits<int>::min());
        return static_cast<int>(s);
    }

    file_path_ = filepath;
    file_size_ = s;
    file_handle_ = file_handle;

    return 0;
}


int SFILE::Open(const ::std::string& filepath) const
{
    if ( is_opened() )
    {
        int r = Close();
        if ( r != 0 )
            return r;
    }

    if ( filepath.empty() )
        return -2;

    // Использовать более безопасный с точки зрения компилятора fopen_s нельзя,
    // т.к. он нарушает семантику fopen, в частности, нельзя открыть 1 файл
    // для записи 2 раза — система не разрешит.
    FILE* file = fopen(filepath.c_str(), "rb");
    if ( file == NULL )
    {
        last_error_ = errno;
        return -1;
    }

    const int fd = _fileno(file);
    if ( fd == -1 )
    {
        last_error_ = errno;
        fclose(file);
        return -1;
    }

    const HANDLE file_handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
    if ( file_handle == INVALID_HANDLE_VALUE )
    {
        last_error_ = errno;
        fclose(file);
        return -1;
    }


    file_ = file;
    const fpos64 s = Size();
    if ( s < 0 )
    {
        file_ = NULL;
        fclose(file);
        assert(s >= ::std::numeric_limits<int>::min());
        return static_cast<int>(s);
    }

    file_path_ = filepath;
    file_size_ = s;
    file_handle_ = file_handle;

    return 0;
}



int SFILE::Resize(const fpos64 max_off)
{
    if ( is_opened() == false || max_off < 0 )
        return -2;

    if ( max_off == file_size_ )
        return 0;


    // Если не сделать fflush перед преаллокацией — при последующем чтении
    // будут считаны неверные данные.
    // С другой стороны, если вызвать здесь Flush, который вызывает еще и
    // FlushFileBuffers исходя из тестов скорость работы pcfs падает в 2-3 раза.
    // Таким образом, вызов fflush здесь — компромисс между скоростью и
    // корректностью.
    if ( fflush(file_) == EOF )
    {
        last_error_ = errno;
        return -1;
    }



    /* http://msdn.microsoft.com/en-us/library/aa365531(v=VS.85).aspx
     * http://msdn.microsoft.com/en-us/library/aa365544(VS.85).aspx
     * http://support.microsoft.com/kb/841551
     * http://www.flamingspork.com/blog/2008/09/08/setfilevaliddata-function-windows-now-with-added-fail/
     * http://www.flexhex.com/docs/articles/sparse-files.phtml
     * http://technet.microsoft.com/en-us/sysinternals/bb897428.aspx
     *
     * Есть несколько способов преаллокации:
     *  - fwrite(nulls) -- ручная преаллокация, в результате которой
     *    данные в файл записываются 2 раза: сначала нули, затем данные.
     *
     *  - _chsize() -- исходя из тестов, реализовано аналогично fwrite,
     *    но варьируя размер блока в fwrite можно в несколько раз
     *    обогнать данный метод.
     *
     *  - SetFileInformationByHandle / NtSetInformationFile -- на Windows 2008
     *    — аналог chsize. Поддерживается, начиная с Windows Vista.
     *
     *  - SetEndOfFile / fseek + fwrite(byte) -- мгновенная преаллокация.
     *    В Windows файл обладает тремя смещениями:
     *      * смещением конца файла (F)
     *      * смещением конца области на диске (кратно размеру кластера,
     *        недоступно для изменения, вычисляется аллокатором ntfs) (A)
     *      * смещение конца данных в файле (E).
     *    E <= F <= A.
     *    Первый параметр изменяется функцией SetEndOfFile() и отображается
     *    системой в свойствах файла.
     *    Третий -- SetFileValidData() и позицией последнего записанного
     *    пользователем байта.
     *    По умолчанию в Windows файлы не разреженные (sparse), т.е. блоки
     *    на диске действительно выделяются, а не заменяются "дырками".
     *    Чтение с диска происходит только для смещений, меньших E.
     *    Для смещений в интервале E < o < F генерируются нули.
     *    Тесты-примеры (выполняются с новым файлом):
     *      - SetEndOfFile + чтение файла -- мгновенное считываются нули.
     *      - SetEndOfFile + fseek(в середину) + fwrite(1 байт) -- половина
     *        файла читается с диска, половина генерируется в памяти.
     *      - SetEndOfFile + SetFileValidData(весь файл) -- весь с диска.
     *      - fseek + fwrite(1 байт) + чтение файла -- весь с диска.
     *      - SetEndOfFile + fwrite всего файла не медленнее, чем просто
     *        fwrite всего файла, но в первом случае место зарезервировано.
     *    Таким образом, данным способом обеспечивается:
     *      - безопасность: нельзя считать чужие данные
     *      - резервирование: блоки выделяются, а не заменяются дырками
     *      - скорость: не происходит зануления выделенной области
     *      - меньшая фрагментация файла: сразу выделяются все кластеры.
     *
     *  - SetEndOfFile + SetFileValidData -- мгновенная преаллокация,
     *    выделяющая область с данными на диске. Данные передается во
     *    владение файлу, так что операционной системе не потребуются
     *    никакие ухищрения при дальнейшей работе с файлом.
     *    Требует специальных прав и не работает в Windows 7/2008/.
     *    Чтение файла — с диска.
     *
     * Во всех случаях запись в полученный файл занимает одинаковое время. */

    LARGE_INTEGER moff;
    moff.QuadPart = max_off;
    if ( SetFilePointerEx(file_handle_, moff, NULL, FILE_BEGIN) == FALSE ||
         SetEndOfFile(file_handle_) == FALSE )
    {
        last_error_ = GetLastError();
        return -1;
    }

    file_size_ = max_off;
    return 0;
}


int SFILE::Flush()
{
    if ( is_opened() == false )
        return -2;

    if ( fflush(file_) == EOF )
    {
        last_error_ = errno;
        return -1;
    }

    if ( ::FlushFileBuffers(file_handle_) == FALSE )
    {
        last_error_ = GetLastError();
        return -1;
    }
    return 0;
}


int SFILE::Fseek(const fpos64 offset, const int origin) const
{
    return _fseeki64(file_, offset, origin);
}


fpos64 SFILE::Ftell() const
{
    return _ftelli64(file_);
}

#pragma warning(pop)



#elif defined(__linux__)

int SFILE::Create(const ::std::string& filepath, const int mode /* = 0644 */)
{
    if ( is_opened() )
    {
        const int r = Close();
        if ( r != 0 )
            return r;
    }

    if ( filepath.empty() || ( ( mode >= 0 && mode <= 777 ) == false ) )
        return -2;

    const int fd = open(filepath.c_str(),
                        O_CREAT | O_EXCL | O_TRUNC | O_LARGEFILE | O_RDWR,
                        mode);
    if ( fd == -1 )
    {
        last_error_ = errno;
        return -1;
    }

    FILE* file = fdopen(fd, "r+b");
    if ( file == NULL )
    {
        last_error_ = errno;
        close(fd);
        return -1;
    }

    file_ = file;
    file_path_ = filepath;
    file_size_ = 0;
    file_handle_ = fd;

    return 0;
}


int SFILE::Open(const ::std::string& filepath)
{
    if ( is_opened() )
    {
        const int r = Close();
        if ( r != 0 )
            return r;
    }

    if ( filepath.empty() )
        return -2;

    FILE* file = fopen(filepath.c_str(), "r+b");
    if ( file == NULL )
    {
        last_error_ = errno;
        return -1;
    }

    const int fd = fileno(file);
    if ( fd == -1 )
    {
        last_error_ = errno;
        fclose(file);
        return -1;
    }

    file_ = file;
    const fpos64 s = Size();
    if ( s < 0 )
    {
        file_ = NULL;
        fclose(file);
        assert(s >= ::std::numeric_limits<int>::min());
        return static_cast<int>(s);
    }

    file_path_ = filepath;
    file_size_ = s;
    file_handle_ = fd;

    return 0;
}


int SFILE::Open(const ::std::string& filepath) const
{
    if ( is_opened() )
    {
        int r = Close();
        if ( r != 0 )
            return r;
    }

    if ( filepath.empty() )
        return -2;

    FILE* file = fopen(filepath.c_str(), "rb");
    if ( file == NULL )
    {
        last_error_ = errno;
        return -1;
    }

    const int fd = fileno(file);
    if ( fd == -1 )
    {
        last_error_ = errno;
        fclose(file);
        return -1;
    }

    file_ = file;
    const fpos64 s = Size();
    if ( s < 0 )
    {
        file_ = NULL;
        fclose(file);
        assert(s >= ::std::numeric_limits<int>::min());
        return static_cast<int>(s);
    }

    file_path_ = filepath;
    file_size_ = s;
    file_handle_ = fd;

    return 0;
}


int SFILE::Resize(const fpos64 max_off)
{
    if ( is_opened() == false || max_off < 0 )
        return -2;

    if ( max_off == file_size_ )
        return 0;

    if ( fflush(file_) == EOF )
    {
        last_error_ = errno;
        return -1;
    }

    /* В linux есть несколько способов преаллокации:
     *  - ручной через fwrite
     *  - posix_fallocate, в зависимости от реализации и файловой системы
     *    реализующийся или как явная запись нулей, или изменением служебных
     *    структур данных ф.с.
     *  - новый (2009 год) вызов fallocate, доступный пока только через
     *    syscall, работающий, например, на ext4 мгновенно резервирующий
     *    блоки на диске.
     * В будущем рекомендуется пользоваться posix_fallocate, так как в
     * зависимости от доступности на системе fallocate он будет определять
     * алгоритм. */
    // fallocate только увеличивает размер
    if ( max_off < file_size_ )
    {
        int f = ftruncate(file_handle_, max_off);
        if ( f == -1 )
        {
            last_error_ = errno;
            return -1;
        }
        file_size_ = max_off;
        return 0;
    }

    int r = 0;
#if defined(SYS_fallocate)  // из linux/falloc.h
    const int resize_mode = 0;
    const loff_t loff(file_size_);
    const loff_t llen(max_off - file_size_);

    r = syscall(SYS_fallocate, file_handle_, resize_mode, loff, llen);
    if ( r == 0 )
    {
        file_size_ = max_off;
        return 0;
    }

    // ФС поддерживает fallocate, но произошла ошибка во время его выполнения
    else if ( errno != EOPNOTSUPP && errno != ENOSYS )
    {
        last_error_ = errno;
        return r;
    }
    else
#endif
#if defined(_XOPEN_SOURCE) && (_XOPEN_SOURCE >= 600)
    {
        const off_t off(file_size_);
        const off_t len(max_off - file_size_);
        r = posix_fallocate(file_handle_, off, len);
        if ( r == 0 )
        {
            file_size_ = max_off;
            return 0;
        }
        else
        {
            last_error_ = r;
            return -1;
        }
    }
#endif
    return -1;
}


int SFILE::Flush()
{
    if ( is_opened() == false )
        return -2;

    if ( fflush(file_) == EOF ||
         fdatasync(file_handle_) == -1 )
    {
        last_error_ = errno;
        return -1;
    }
    return 0;
}


int SFILE::Fseek(const fpos64 offset, const int origin) const
{
    return fseeko(file_, offset, origin);
}


fpos64 SFILE::Ftell() const
{
    return ftello(file_);
}

#endif  // _WIN32

}  // libv

#endif  // LIBV_SFILE_H_
